package com.tang.common.utils;

import cn.hutool.core.date.DateTime;
import cn.hutool.core.date.DateUtil;
import com.tang.common.constant.ParamName;
import com.tang.common.exception.GlobalException;
import com.tang.common.utils.cache.ParamCache;
import io.jsonwebtoken.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

/**
 * JWT的token包含三部分数据：
 *
 *
 *  声明类型-加密算法 + 负载（公共负载，私有负载，预定义负载） + 密钥
 *   base64加密             base64加密                  前两部分加密结果和盐值进行加密算法加密
 *
 * Header：头部，通常头部有两部分信息：
 * 声明类型type，这里是JWT（type=jwt）
 * 加密算法，自定义(rs256/base64/hs256)
 * 我们会对头部进行base64加密（可解密），得到第一部分数据
 * Payload：载荷，就是有效数据，一般包含下面信息：
 * 用户身份信息-userid,username（注意，这里因为采用base64加密，可解密，因此不要存放敏感信息）
 * 注册声明：如token的签发时间，过期时间，签发人等
 * 这部分也会采用base64加密，得到第二部分数据
 * Signature：base64加密，签名，是整个数据的认证信息。一般根据前两步的数据，再加上服务的的密钥（secret，盐）（不要泄漏，最好周期性更换），通过加密算法生成。用于验证整个数据完整和可靠性
 * 一个JWT实际上就是一个字符串，它由三部分组成，头部、载荷与签名。
 * @author tang
 * @date 2021/4/1 13:04
 */
@Slf4j
@Component
public class JwtTokenUtil {

    /**
     * 生成jwt
     * 使用Hs256算法, 私匙使用固定JWT_SEC秘钥
     *
     * @param jwtSec    jwt秘钥 此秘钥一定要保留好在服务端, 不能暴露出去, 否则sign就可以被伪造, 如果对接多个客户端建议改造成多个
     * @param ttlMillis jwt过期时间(毫秒)
     * @param username  用户名 可根据需要传递的信息添加更多, 因为浏览器get传参url限制，不建议放置过多的参数
     * @return
     */
    public static String createJWT(String jwtSec, long ttlMillis, String username) {
        // 指定签名的时候使用的签名算法，也就是header那部分
        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;

        // 生成JWT的时间
        long nowMillis = System.currentTimeMillis();
        Date now = new Date(nowMillis);

        // 创建payload的私有声明（根据特定的业务需要添加）
        Map<String, Object> claims = new HashMap<String, Object>();
        claims.put("username", username);

        // 添加payload声明
        // 设置jwt的body
        JwtBuilder builder = Jwts.builder()
                // 如果有私有声明，一定要先设置这个自己创建的私有的声明，这个是给builder的claim赋值，一旦写在标准的声明赋值之后，就是覆盖了那些标准的声明的
                .setClaims(claims)
                // 设置jti(JWT ID)：是JWT的唯一标识，根据业务需要，这个可以设置为一个不重复的值，主要用来作为一次性token,从而回避重放攻击。
                .setId(UUID.randomUUID().toString())
                // iat: jwt的签发时间
                .setIssuedAt(now)
                // 代表这个JWT的主体，即它的所有人，这个是一个json格式的字符串
                .setSubject(username)
                // 设置签名使用的签名算法和签名使用的秘钥
                .signWith(signatureAlgorithm, jwtSec);
        if (ttlMillis >= 0) {
            long expMillis = nowMillis + ttlMillis;
            Date exp = new Date(expMillis);
            // 设置过期时间
            builder.setExpiration(exp);
        }
        return builder.compact();
    }


    /**
     * Token的解密
     *
     * @param jwtSec jwt秘钥 此秘钥一定要保留好在服务端, 不能暴露出去, 否则sign就可以被伪造, 如果对接多个客户端建议改造成多个
     * @param token  加密后的token
     * @return
     */
    public static Claims parseJwt(String jwtSec, String token) {
        // 得到DefaultJwtParser
        Claims claims = null;
        try {
            claims = Jwts.parser()
                    // 设置签名的秘钥
                    .setSigningKey(jwtSec)
                    // 设置需要解析的jwt
                    .parseClaimsJws(token).getBody();
        }catch (Exception e){
            return null;
        }
        /*catch (ExpiredJwtException e) {
            throw new GlobalException("token已过期");
        } catch (UnsupportedJwtException e) {
            throw new GlobalException("不支持该token");
        } catch (MalformedJwtException e) {
            throw new GlobalException("token格式错误");
        } catch (SignatureException e) {
            throw new GlobalException("token签名异常");
        } catch (IllegalArgumentException e) {
            throw new GlobalException("token参数异常");
        }*/
        return claims;
    }

    /**
     * Token的解密，获取用户名
     *
     * @param jwtSec jwt秘钥 此秘钥一定要保留好在服务端, 不能暴露出去, 否则sign就可以被伪造, 如果对接多个客户端建议改造成多个
     * @param token  加密后的token
     * @return
     */
    public static String parseJwtUserName(String jwtSec, String token) {
        Claims claims = parseJwt(jwtSec, token);
        if (claims == null){
            return null;
        }
        return claims.get("username",String.class);
    }

    public static boolean refresh(String jwtSec, String token){
        Claims claims = parseJwt(jwtSec, token);
        if (claims == null){
            return false;
        }
        //获得过期时间
        Date expiration = claims.getExpiration();
        //这里设置为离过期10分钟就可以刷新令牌了
        String param = ParamCache.getValue(ParamName.TOKEN_REFRESH_TIME);
        DateTime dateTime = DateUtil.offsetMinute(expiration, - Integer.parseInt(param));
        log.info("下次token刷新时间:{}",dateTime.toString());
        return dateTime.before(new Date());
    }

}
